[Previous] [Next]

The TreeView Control

The TreeView control is probably the first Windows common control that users become acquainted with because it's the control Windows Explorer is based on. Basically, the TreeView control displays a hierarchy of items. A plus sign beside an item indicates that it has one or more child items. An item that has child items can be expanded to show them or collapsed to hide them. This can be done interactively by the user or via code.

The Visual Basic 6 version of the TreeView control has a number of improvements and now supports check boxes beside each item and full row selection. Moreover, individual nodes can have different Bold, Foreground, and Background attributes.

The TreeView control exposes a Nodes collection, which in turn includes all the Node objects that have been added to the control. Each individual Node object exposes a number of properties that let you define the look of the control. Typically, a TreeView control has one single root Node object, but you can also create multiple Node objects at the root level.

Setting Design-Time Properties

Immediately after creating a TreeView control on a form, you should display its Properties dialog box (shown in Figure 10-5), which you do by right-clicking on the control and selecting the Properties menu item. Of course, you can also set properties that appear in this page at run time, but you rarely need to change the appearance of a TreeView control once it has been displayed to the user.

The Style property affects which graphical elements will be used inside the control. A TreeView control can display four graphical elements: the text associated with each Node object, the picture associated with each Node object, a plus or minus sign beside each Node object (to indicate whether the Node is in collapsed or expanded state), and the lines that go from each Node object to its child objects. The Style property can be assigned one of eight values, each one representing a different combination of these four graphical elements. In most cases, you use the default value, 7-tvwTreelinesPlusMinusPictureText, which displays all graphical elements.

Figure 10-5. The General tab of the Properties dialog box of a TreeView control.

The LineStyle property affects how lines are drawn. The value 0-tvwTreeLines doesn't display lines among root Node objects (this is the default setting), whereas the value 1-tvwRootLines also displays lines among all root Nodes and makes them appear as if they were children of a fictitious Node located at an upper level. The Indentation property states the distance in twips between vertical dotted lines.

The LabelEdit property affects how the end user can modify the text associated with each Node object. If it's assigned the value 0-tvwAutomatic (the default), the end user can edit the text by clicking on the Node at run time; if it's assigned the value 1-tvwManual, the edit operation can be started only programmatically, by your issuing a StartLabelEdit method.

The ImageList combo box lets you select which ImageList control will be used to retrieve the images of individual Node objects. The combo box lists all the ImageList controls located on the current form.

TIP
You can associate a TreeView control (or any control) with an ImageList control located on another form by making the assignment at run time, as shown in this code:

Private Sub Form_Load()
    Set TreeView1.ImageList = AnotherForm.ImageList1
End Sub

This technique allows you to use a group of bitmaps and icons in all the forms of your application without having to duplicate them and thus shrink the size of the EXE file. This way, you save memory and resources at run time.

The HideSelection property determines whether the selected Node object will continue to be highlighted when the TreeView control loses the focus. The PathSeparator property states which character or string should be used in the FullPath property of the Node object. The default value for the PathSeparator property is the backslash character. For example, if you have a root Node labeled "Root" and a child Node labeled "FirstChild", the FullPath property of the child Node will be "Root\FirstChild".

The Sorted property states whether Nodes in the control are automatically sorted in alphabetical order. The documentation omits an important detail: This property affects only how root Node objects are sorted but has no effect on the order of child Node objects at lower levels. If you want all the branches of the tree to be sorted, you should set the Sorted properties of all individual Node items to True.

The TreeView control that comes with Visual Basic 6 adds a few interesting properties not available in previous versions of the language. The FullRowSelect property, if True, causes a Node of the control to be selected if the user clicks anywhere on its row. (By default, this property is False, in which case an item can be selected only with a click over it or its plus or minus symbol.)

If you set the Checkboxes property to True, a check box appears beside each Node object so that the end user can select multiple Node objects.

By default, you need to double-click on Node items to expand or collapse them (or click on the plus or minus sign, if present), and you can expand and collapse any number of tree branches independently of one another. But if you set the SingleSel property to True, the control's behavior is different: You expand and collapse items with a single click—that is, as soon as you select them. Moreover, when you expand a Node, the item that was previously expanded is automatically collapsed.

The Scroll property determines whether the TreeView control displays a vertical or horizontal scroll bar if necessary. The default value is True, but you can set it to False to disable this behavior (even though, honestly, I can't find a reason why you would want to do that).

Finally the HotTracking property lets you create a Web-like user interface. If you set this property to True, the cursor changes into a hand when the mouse passes over the Node object and the TreeView control underlines the Node's Text property.

Run-Time Operations

To fully exploit the potential of the TreeView control, you must learn to deal with the Nodes collections and the many properties and methods of Node objects.

Adding Node objects

One of the shortcomings of the TreeView control is that you can't add items at design time as you can with ListBox and ComboBox controls. You can add Node objects only at run time using the Add method of the Nodes collection.The Add method's syntax is the following:

Add([Relative],[Relationship],[Key],[Text],[Image],[SelectedImage]) As Node

Relative and Relationship indicate where the new Node should be inserted. Key is its string key in the Nodes collection, Text is the label that will appear in the control, and Image is the index or the string key in the companion ImageList control of the image that will appear beside the Node. SelectedImage is the index or key of the image that will be used when the Node is selected. For example, if you're creating a TreeView control that mimics Windows Explorer and its directory objects, you might write something like this:

Dim nd As Node
Set nd  = Add(, , ,"C:\System", "Folder", "OpenFolder")

To place the new Node in a given position in the tree, you must provide the first two arguments. The first argument specifies an existing item in the Nodes collection by its numerical index or string key; the second argument states the relationship between the Node being added and its relative. Such a relationship can be 0-tvwFirst, in which the new Node becomes the first item at the level of its relative—in other words, it becomes the first sibling of the relative Node. Or the relationship can be 1-tvwLast (the new Node becomes the last sibling of the relative Node); 2-tvwNext (default, the new Node is added immediately after the relative Node, at the same level in the hierarchy); 3-tvwPrevious (the new Node is inserted immediately before the relative Node, at the same level in the hierarchy); or 4-tvwChild (the new Node becomes a child of the relative Node and is inserted after all existing child nodes).

Here's an example of a routine that fills a TreeView control with the structure of an MDB file—that is, the tables it contains and the fields for each table. The routine accepts a reference to the control in its second argument so that you can easily reuse it in your applications. The third argument passed to the routine is a Boolean value that states whether system tables should be displayed:

Sub ShowDatabaseStructure(MdbFile As String, TV As TreeView, _
    ShowSystemTables As Boolean)
    Dim db As DAO.Database, td As DAO.TableDef, fld As DAO.Field
    Dim nd As Node, nd2 As Node
    ' Clear the current contents of the TreeView control.
    TV.Nodes.Clear
    ' Open the database.
    Set db = DBEngine.OpenDatabase(MdbFile)
    ' Add the root Node, and then expand it to show the tables.
    Set nd = TV.Nodes.Add(, , "Root", db.Name, "Database")
    nd.Expanded = True

    ' Explore all the tables in the database.
    For Each td In db.TableDefs
        ' Discard system tables if user isn't interested in them.
        If (td.Attributes And dbSystemObject) = 0 Or ShowSystemTables Then
            ' Add the table under the Root object.
            Set nd = TV.Nodes.Add("Root", tvwChild, , td.Name, "Table")
            ' Now add all the fields.
            For Each fld In td.Fields
                Set nd2 = TV.Nodes.Add(nd.Index, tvwChild, , _
                    fld.Name, "Field")
                Next
            End If
    Next
    db.Close
End Sub

Note that the routine doesn't include any error handler: if the file doesn't exist or is an invalid or corrupted MDB archive, the error is simply returned to the caller. It's usual to show a TreeView control with the root object already expanded in order to save the end user a mouse click. The routine does this by setting the root Node object's Expanded property to True.

Appearance and visibility

You can control the appearance of individual Node objects by setting their ForeColor, BackColor, and Bold properties, the effects of which are shown in Figure 10-6. This new feature permits you to visually convey more information about each Node. Typically, you set these properties when you add an item to the Nodes collection:

With TV.Nodes.Add(, , , "New Node")
    .Bold = True
    .ForeColor = vbRed
    .BackColor = vbYellow
End With

Figure 10-6. Effects of the ForeColor, BackColor, and Bold properties of Node objects, as well as of the Checkboxes property of the TreeView control.

Each Node object has three images associated with it, and the Node's current state determines which image is displayed. The Image property sets or returns the index of the default image; the SelectedImage property sets or returns the index of the image used when the Node is selected; the ExpandedImage property sets or returns the index of the image used when the Node is expanded. You can set the first two properties in the Nodes collection's Add method, but you must explicitly assign the ExpandedImage property after you've added the item to the collection.

You can learn whether a particular Node is currently visible by querying its Visible property. A Node item can be invisible because it belongs to a tree branch that's in a collapsed state or because it has scrolled away from the visible portion of the control. This property is read-only, but you can force the visibility state of a Node by executing its EnsureVisible method:

' Scroll the TreeView, and expand any parent Node if necessary.
If aNode.Visible = False Then aNode.EnsureVisible

You can learn how many Nodes are visible in the control by executing TreeView's GetVisibleCount method.

You have two ways to determine whether a Node is currently the selected Node object in the control—either by querying its Selected property or by testing the TreeView's SelectedItem property:

' Check whether aNode is the Node currently selected (two
' equivalent ways).
' First way:
If aNode.Selected Then MsgBox "Selected"
' Second way:
If TreeView1.SelectedItem Is aNode Then MsgBox "Selected"

' Make aNode the currently selected Node (two equivalent ways).
' First way:
aNode.Selected = True
' Second way:
Set TreeView1.SelectedItem = aNode

Showing information about a Node

Users expect the program to do something when they click on a Node object in the TreeView control—for example, to display some information related to that object. To learn when a Node is clicked, you have to trap the NodeClick event. You can determine which Node has been clicked by looking at the Index or Key property of the Node parameter passed to the event procedure. In a typical situation, you store information about a Node in an array of String or UDT items:

Private Sub TreeView1_NodeClick(ByVal Node As MSComctlLib.Node)
    ' info() is an array of strings that hold nodes' descriptions.
    lblData.Caption = info(Node.Index) 
End Sub

The NodeClick event differs from the regular Click event in that the latter fires whenever the user clicks on the TreeView control, whereas the former is activated only when the user clicks on a Node object.

The previous code snippet has a flaw: In general, the Index property of a Node object can't be trusted because it can change when other Node objects are removed from the Nodes collection. For this reason, you should rely exclusively on the Key property, which is guaranteed not to vary after the Node has been added to the collection. For example, you can use the Key property to search for an item in a standard Collection object, where you store information that's related to the Node. Here's a better technique: You store the data in the Tag property of the Node object so that you don't have to worry about removing items from the control's Node collection. The BrowMdb.vbp project on the companion CD includes a revised version of the ShowDatabaseStructure routine to show properties and attributes of all the Field and TableDef objects displayed in the TreeView control, as you can see in Figure 10-7.

Click to view at full size.

Figure 10-7. A simple browser for Microsoft Jet databases.

Editing Node text

By default, the user can click on a Node object to enter Edit mode and indirectly change the Node object's Text property. If you don't like this behavior, you can set the LabelEdit property to 1-tvwManual. In this case, you can enter Edit mode only by programmatically executing a StartLabelEdit method.

Regardless of the value of the LabelEdit property, you can trap the instant when the user begins editing the current value of the Text property by writing code in the BeforeLabelEdit event procedure. When this event fires, you can discover which Node is currently selected by using the TreeView's SelectedItem property, and you can cancel the operation by setting the event's Cancel parameter to True:

Private Sub TreeView1_BeforeLabelEdit(Cancel As Integer)
    ' Prevent the root Node's Text property from editing.
    If TreeView1.SelectedItem.Key = "Root" Then Cancel = True
End Sub

Similarly, you can find out when the user has completed the editing and reject, if you want to, the new value of the Text property by trapping the AfterLabelEdit event. Typically, you use this event to check whether the new value follows any syntactical rule enforced by the particular object. For example, you can reject empty strings by writing the following code:

Private Sub TreeView1_AfterLabelEdit(Cancel As Integer, _
    NewString As String)
    If Len(NewString) = 0 Then Cancel = True
End Sub

Using check boxes

To display a check box beside each Node in the TreeView control, you simply need to set the control's Checkboxes property to True, either at design time or run time. You can then query or modify the state of each Node using its Checked property:

' Count how many Node objects are checked, and then reset all check boxes.
Dim i As Long, SelCount As Long
For i = 1 To TreeView1.Nodes.Count
    If TreeView1.Nodes(i).Checked Then
        SelCount = SelCount + 1
        TreeView1.Nodes(i).Checked = False
    End If
Next

You can enforce tighter control over what happens when a Node is checked by writing code in the control's NodeChecked event. This event doesn't fire if you modify a Node's Checked property using code:

Dim SelCount As Long     ' The number of selected items

Private Sub TreeView1_NodeCheck(ByVal Node As MSComctlLib.Node)
    ' Display the number of selected Nodes.
    If Node.Checked Then
        SelCount = SelCount + 1
    Else
        SelCount = SelCount _ 1
    End If
    lblStatus = "Selected Items = " & SelCount
End Sub

TIP
If you want to prevent the user from modifying the Checked state of a given Node object, you can't simply reset its Checked property within the NodeCheck event because all changes to this property are lost when the event procedure is exited. You can solve this problem by adding a Timer control on the form and writing this code:

Dim CheckedNode As Node            ' A form-level variable

Private Sub TreeView1_NodeCheck(ByVal Node As MSComctlLib.Node)

    ' Prevent the user from checking the first Node.
    If Node.Index = 1 Then
        ' Remember which Node has been clicked on.
        Set CheckedNode = Node
        ' Let the Timer routine do the job.
        Timer1.Enabled = True
    End If
End Sub

Private Sub Timer1_Timer()
    ' Reset the Checked property, and then go to sleep.
    CheckedNode.Checked = False
    Timer1.Enabled = False
End Sub

This technique is more effective if the Timer's Interval property is set to a small value, such as 10 milliseconds.

Advanced Techniques

The TreeView control is very flexible, but sometimes you have to resort to more advanced and less intuitive techniques to leverage its power.

Loading Nodes on demand

Theoretically, you can load thousands of items into a TreeView control, which is more than an average user is willing to examine. In practice, loading more than a few hundred items makes a program unacceptably slow. Take, for example, the task of loading a directory structure into a TreeView control the way Windows Explorer does it: This simple job requires a lot of time to scan the system's hard disk, and you simply can't have your user wait for this long. In these situations, you might need to resort to a load on demand approach.

Loading items on demand means that you don't add Node objects until you have to display them, one instant before their parent Node is expanded. You can determine when a Node is expanded by trapping the TreeView control's Expand event. (You can also find out when a Node object is collapsed by trapping the control's Collapse event.) The tricky detail is how to let the user know that a Node has one or more child objects without actually adding them. In other words, we need to show a plus sign beside each Node item with children.

It is easy to demonstrate that the TreeView common control is able to display a plus sign beside a Node without child Nodes: Just run Windows Explorer and look at the plus sign beside the icon for the A: floppy drive; it's there even if no subdirectories are on the diskette (and even if no diskette is in the A: drive). Unfortunately, the ability to display a plus sign without adding child Nodes hasn't been exposed in the OCX that comes with Visual Basic and requires some API programming. The technique I will show you, however, does the trick without any API call.

To show a plus sign beside a Node, all you have to do is add a child Node, any child Node. I'll call this a dummy child Node. You need to mark such a dummy Node item in an unambiguous way—for example, by storing a special value in its Text or Tag property. When a Node is eventually expanded, the program checks whether the Node has a dummy child item. If so, the code removes the dummy child and then adds all the actual child Nodes. As you see, the technique is simple, even if its implementation includes some nontrivial code.

Figure 10-8 shows the demonstration program at run time. Its complete source code is on the companion CD, so I'll just illustrate its key routines. The form contains the tvwDir TreeView control and uses the FileSystemObject hierarchy to retrieve the directory structure.

Figure 10-8. A directory browser program that loads TreeView Nodes on demand.

The following DirRefresh procedure is invoked from within the Form_Load event:

Private Sub DirRefresh()
    Dim dr As Scripting.Drive
    Dim rootNode As node, nd As Node
    On Error Resume Next
    
    ' Add the "My Computer" root Node (expanded).
    Set rootNode = tvwDir.Nodes.Add(, , "\\MyComputer", _
        "My Computer", 1)
    rootNode.Expanded = True
    ' Add all the drives; display a plus sign beside them.
    For Each dr In FSO.Drives
        ' Error handling is needed to account for not-ready drives.
        Err.Clear
        Set nd = tvwDir.Nodes.Add(rootNode.Key, tvwChild, dr.Path & "\", _
            dr.Path & " " & dr.VolumeName, 2)
        If Err = 0 Then AddDummyChild nd
    Next
End Sub

Sub AddDummyChild(nd As node)
    ' Add a dummy child Node if necessary.
    If nd.Children = 0 Then
        ' Dummy nodes' Text property is "***".
        tvwDir.Nodes.Add nd.index, tvwChild, , "***"
    End If
End Sub

The previous routine ensures that the form is displayed with the "MyComputer" root Node and all the drives below it. When the user expands a Node, the following event fires:

Private Sub tvwDir_Expand(ByVal node As ComctlLib.node)
    ' A Node is being expanded.
    Dim nd As Node
    ' Exit if the Node had already been expanded or has no children.
    If node.Children = 0 Or node.Children > 1 Then Exit Sub
    ' Also exit if it doesn't have a dummy child Node.
    If node.Child.text <> "***" Then Exit Sub
    ' Remove the dummy child item.
    tvwDir.Nodes.Remove node.Child.index
    ' Add all the subdirs of this Node object.
    AddSubdirs node
End Sub

The tvwDir_Expand procedure uses the Children property of the Node object, which returns the number of its child Nodes, and the Child property, which returns a reference to its first child Node. The AddSubdirs procedure adds all the subdirectories below a given Node. Because each Node's Key property always holds the path corresponding to that Node, it's simple to retrieve the corresponding Scripting.Folder object and then iterate on its SubFolders collection:

Private Sub AddSubdirs(ByVal node As ComctlLib.node)
    ' Add all the subdirectories under a Node.
    Dim fld As Scripting.Folder
    Dim nd As Node
    ' The path in the Node is held in its key property, so it's easy
    ' to cycle on all its subdirectories.
    For Each fld In FSO.GetFolder(node.Key).SubFolders
        Set nd = tvwDir.Nodes.Add(node, tvwChild, fld.Path, fld.Name, 3)
        nd.ExpandedImage = 4
        ' If this directory has subfolders, add a plus sign.
        If fld.SubFolders.Count Then AddDummyChild nd
    Next
End Sub

Even if this code can be used only for loading and displaying a directory tree, you can easily modify it to work with any other type of data that you want to load on demand into a TreeView control.

A great way to use this technique is for browsing databases in a hierarchical format. Take the ubiquitous Biblio.Mdb as an example: You can load all the publishers' names in the TreeView control and show their related titles only when the user expands a Node. This is much faster than preloading all the data in the control and offers a clear view of how records are related. I've provided a sample program that uses this technique on the companion CD.

Searching the Nodes collection

When you need to extract information from a TreeView control, you have to search the Nodes collection. In most cases, however, you can't simply scan it from the first to the last item because this order would reflect the sequence in which Node objects were added to the collection and doesn't take into account the relationships among them.

To let you visit all the items in a TreeView control in a hierarchical order, each Node object exposes a number of properties that return references to its relatives. We have already seen the Child property, which returns a reference to the first child, and the Children property, which returns the number of child Nodes. The Next property returns a reference to the next Node at the same level (the next sibling Node), and the Previous property returns a reference to the previous Node at the same level (the previous sibling Node). The FirstSibling and the LastSibling properties return a reference to the first and last Node, respectively, that are at the same level as the Node being examined. Finally the Parent property returns a reference to the Node object one level above, and the Root property returns a reference to the root of the hierarchy tree that the Node being queried belongs to. (Remember, there can be multiple root Nodes.) You can use these properties to test where a given Node appears in the hierarchy. Here are a few examples:

' Check whether a Node has no children (two possible approaches).
If Node.Children = 0 Then MsgBox "Has no children"
If Node.Child Is Nothing Then MsgBox "Has no children"
' Check whether a Node is the first child of its parent (two approaches).
If Node.Previous Is Nothing Then MsgBox "First Child" 
If Node.FirstSibling Is Node Then MsgBox "First Child"
' Check whether a Node is the last child of its parent (two approaches).
If Node.Next Is Nothing Then MsgBox "Last Child" 
If Node.LastSibling Is Node Then MsgBox "Last Child"
' Check whether a Node is the root of its own tree (two approaches).
If Node.Parent Is Nothing Then MsgBox "Root Node" 
If Node.Root Is Node Then MsgBox "Root Node" 
' Get a reference to the first root Node in the control.
Set RootNode = TreeView1.Nodes(1).Root.FirstSibling

Not surprisingly, the majority of routines you write to search the Nodes collection are recursive. Typically, you start with a Node object, get a reference to its first child Node, and then recursively call the routine for all its children. The following routine is an example of this technique. Its purpose is to build a text string that represents the contents of a TreeView control or of one of its subtrees. Each line in the string represents a Node object, indented with 0 or more tab characters that reflect the corresponding nesting level. The routine can return a string for all the Nodes or for just the items that are actually visible (that is, those whose parents are expanded):

' Convert the contents of a TreeView control into a string.
' If a Node is provided, it only searches a subtree.
' If last argument is False or omitted, all items are included.
Function TreeViewToString(TV As TreeView, Optional StartNode As Node, _
    Optional OnlyVisible As Boolean) As String
    Dim nd As Node, childND As Node
    Dim res As String, i As Long
    Static Level As Integer
    
    ' Exit if there are no Nodes to search.
    If TV.Nodes.Count = 0 Then Exit Function
    ' If StartNode is omitted, start from the first root Node.
    If StartNode Is Nothing Then
        Set nd = TV.Nodes(1).Root.FirstSibling
    Else
        Set nd = StartNode
    End If
    
    ' Output the starting Node.
    res = String$(Level, vbTab) & nd.Text & vbCrLf
    ' Then call this routine recursively to output all child Nodes.
    ' If OnlyVisible = True, do this only if this Node is expanded.
    If nd.Children And (nd.Expanded Or OnlyVisible = False) Then
        Level = Level + 1
        Set childND = nd.Child
        For i = 1 To nd.Children
            res = res & TreeViewToString(TV, childND, OnlyVisible)
            Set childND = childND.Next
        Next
        Level = Level - 1
    End If
    
    ' If searching the whole tree, we must account for multiple roots.
    If StartNode Is Nothing Then
        Set nd = nd.Next
        Do Until nd Is Nothing
            res = res & TreeViewToString(TV, nd, OnlyVisible)
            Set nd = nd.Next
        Loop
    End If
    TreeViewToString = res
End Function

Figure 10-9 shows a demonstration program that uses this routine and the resulting string loaded into Notepad. You can embellish the TreeViewToString procedure in many ways. For instance, you can create a routine that prints the contents of a TreeView control (including all connecting lines, bitmaps, and so on). Using a similar approach, you can build a routine that saves and restores the current state of a TreeView control, including the Expanded attribute of all Node objects.

Click to view at full size.

Figure 10-9. A demonstration program that uses the TreeViewToString routine.

Implementing drag-and-drop

Another common operation that you might want to perform on TreeView controls is drag-and-drop, typically to copy or move a portion of a hierarchy. Implementing a drag-and-drop routine isn't simple, though. First you have to understand which Node the drag operation starts and ends on, and then you have to physically copy or move a portion of the Nodes collection. You must also prevent incorrect operations, as Windows Explorer does when you try to drag a directory onto one of its subfolders.

The TreeView control does offer a few properties and methods that are particularly useful for implementing a drag-and-drop routine. You can use the HitTest method to determine which Node object is located at a given pair of coordinates. (Typically, you use this method in a MouseDown event to pinpoint the source Node of the drag operation.) During the drag operation, you use the DropHighlight property to highlight the Node that's under the mouse cursor so that you can provide the user with a clue to the potential target Node of the operation.

Figure 10-10 shows a demonstration program, provided on the companion CD, that lets you experiment with drag-and-drop between TreeView controls or within the same TreeView control. The two controls on the form belong to a control array, so the same code works whichever is the source or the target control.

Click to view at full size.

Figure 10-10. You can use drag-and-drop to move or copy subtrees between controls or even within the same TreeView control.

As usual, the drag operation is started in the MouseDown event procedure:

' The source control
Dim SourceTreeView As TreeView
' The source Node object
Dim SourceNode As Node
' The state of Shift key during the drag-and-drop operation
Dim ShiftState As Integer

Private Sub TreeView1_MouseDown(Index As Integer, _
    Button As Integer, Shift As Integer, x As Single, y As Single)
    ' Check whether we are starting a drag operation.
    If Button <> 2 Then Exit Sub
    ' Set the Node being dragged, or exit if there is none.
    Set SourceNode = TreeView1(Index).HitTest(x, y)
    If SourceNode Is Nothing Then Exit Sub
    ' Save values for later.
    Set SourceTreeView = TreeView1(Index)
    ShiftState = Shift
    ' Start the drag operation.
    TreeView1(Index).OLEDrag
End Sub

The OLEStartDrag event procedure is where you decide whether you're moving or copying items, depending on the state of the Ctrl key:

Private Sub TreeView1_OLEStartDrag(Index As Integer, _
    Data As MSComctlLib.DataObject, AllowedEffects As Long)
    ' Pass the Key property of the Node being dragged.
    ' (This value is not used; we can actually pass anything.)
    Data.SetData SourceNode.Key
    If ShiftState And vbCtrlMask Then
        AllowedEffects = vbDropEffectCopy
    Else
        AllowedEffects = vbDropEffectMove
    End If
End Sub

In the OLEDragOver event procedure, you offer feedback to the user by highlighting the Node under the mouse in the target control. (The source and the target controls might coincide.)

Private Sub TreeView1_OLEDragOver(Index As Integer, _
    Data As MSComctlLib.DataObject, Effect As Long, Button As Integer, _
    Shift As Integer, x As Single, y As Single, State As Integer)
    ' Highlight the Node the mouse is over.
    Set TreeView1(Index).DropHighlight = TreeView1(Index).HitTest(x, y)
End Sub

Finally you have to implement the OLEDragDrop routine, which is the most complex of the group. First you must figure out whether the mouse is over a Node in the target control. If so, the source Node becomes a child of the target Node; otherwise, the source Node becomes a root Node in the target control. If the source and target controls coincide, you must also ensure that the target Node isn't a child or grandchild of the source Node, which would trap you in an endless loop.

Private Sub TreeView1_OLEDragDrop(Index As Integer, _
    Data As MSComctlLib.DataObject, Effect As Long, Button As Integer, _
    Shift As Integer, x As Single, y As Single)
    Dim dest As Node, nd As Node
    ' Get the target Node.
    Set dest = TreeView1(Index).DropHighlight
    
    If dest Is Nothing Then
        ' Add the Node as the root of the target TreeView.
        Set nd = TreeView1(Index).Nodes.Add(, , , SourceNode.Text, _
            SourceNode.Image)
    Else
        ' Check that the destination isn't a descendant of the source
        ' Node.
        If SourceTreeView Is TreeView1(Index) Then
            Set nd = dest
            Do
                If nd Is SourceNode Then
                    MsgBox "Unable to drag Nodes here", vbExclamation
                    Exit Sub
                End If
                Set nd = nd.Parent
            Loop Until nd Is Nothing
        End If
        Set nd = TreeView1(Index).Nodes.Add(dest.Index, tvwChild, , _ 
            SourceNode.Text, SourceNode.Image)
    End If
    nd.ExpandedImage = 2: nd.Expanded = True

    ' Copy the subtree from source to target control.
    CopySubTree SourceTreeView, SourceNode, TreeView1(Index), nd
    ' If this is a move operation, delete the source subtree.
    If Effect = vbDropEffectMove Then
        SourceTreeView.Nodes.Remove SourceNode.Index
    End If
    Set TreeView1(Index).DropHighlight = Nothing
End Sub

The CopySubTree recursive procedure performs the actual Copy command. (A move operation consists of a copy operation followed by a delete operation.) It accepts a reference to source and target TreeView controls, so you can easily recycle it in other applications:

Sub CopySubTree(SourceTV As TreeView, sourceND As Node, _
    DestTV As TreeView, destND As Node)
    ' Copy or move all children of a Node to another Node.
    Dim i As Long, so As Node, de As Node
    If sourceND.Children = 0 Then Exit Sub
    
    Set so = sourceND.Child
    For i = 1 To sourceND.Children
        ' Add a Node in the destination TreeView control.
        Set de = DestTV.Nodes.Add(destND, tvwChild, , so.Text, _
            so.Image, so.SelectedImage)
        de.ExpandedImage = so.ExpandedImage
        ' Now add all the children of this Node in a recursive manner.
        CopySubTree SourceTV, so, DestTV, de
        ' Get a reference to the next sibling.
        Set so = so.Next
    Next
End Sub

You don't need a recursive procedure to delete a subtree because if you delete a Node object, all its child nodes are automatically deleted too.